﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-21 20:30
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using BassDrop.Core.Common.Data;
using BassDrop.Core.Common.General.Collections;
using BassDrop.Core.Common.General.Events;
using BassDrop.Core.Common.Localization;

namespace BassDrop.Core.Common.Media.Playlists
{
	/// <summary>
	/// Class storing a list of MediaTracks and PlaylistItems
	/// </summary>
	public class Playlist : SuperCollection<PlaylistItemBase>
	{
		#region Types

		/// <summary>
		/// How the playlist is repeated
		/// </summary>
		public enum RepeatModeType
		{
			/// <summary>
			/// Playback stops at the end
			/// </summary>
			NoRepeat,
			/// <summary>
			/// The selected track is repeated
			/// </summary>
			RepeatOne,
			/// <summary>
			/// The entire playlist is repeated
			/// </summary>
			RepeatAll
		}

		#endregion Types

		private Random m_shuffleRandom = new Random();

		#region Properties

		/// <summary>
		/// The file describing this playlist, if any
		/// </summary>
		public PlaylistFile PlaylistFile
		{
			get;
			set;
		}

		/// <summary>
		/// The name of this playlist
		/// </summary>
		public string Name
		{
			get;
			private set;
		}

		private bool m_shuffle = false;

		/// <summary>
		/// Is the playlist played in shuffle
		/// </summary>
		public bool Shuffle
		{
			get
			{
				return m_shuffle;
			}
			set
			{
				if ( m_shuffle != value )
				{
					m_shuffle = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "Shuffle" ) );
				}
			}
		}

		private RepeatModeType m_repeatMode = RepeatModeType.RepeatAll;

		/// <summary>
		/// How is the playlist repeated
		/// </summary>
		public RepeatModeType RepeatMode
		{
			get
			{
				return m_repeatMode;
			}
			set
			{
				if ( m_repeatMode != value )
				{
					m_repeatMode = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "RepeatMode" ) );
				}
			}
		}

		/// <summary>
		/// Currently selected track index in the playlist
		/// </summary>
		private int m_selectedIndex;
		/// <summary>
		/// Currently selected track index in the playlist
		/// </summary>
		public int SelectedIndex
		{
			get
			{
				lock ( ((ICollection) this).SyncRoot )
				{
					return m_selectedIndex;
				}
			}
			set
			{
				SetSelectedIndex( value, false );
			}
		}

		/// <summary>
		/// Sets the value of the selected index property
		/// </summary>
		/// <param name="newIndex">the index to set</param>
		/// <param name="listChanged">
		///		If true, the track won't restart if it didn't change
		///	</param>
		private void SetSelectedIndex( int newIndex, bool listChanged )
		{
			bool fireEvent = false;
			lock ( ((ICollection) this).SyncRoot )
			{
				if ( m_selectedIndex != newIndex )
				{
					if ( newIndex < 0 || newIndex >= Count )
					{
						// 0 is always allowed because of empty playlists
						if ( newIndex != 0 )
						{
							throw new IndexOutOfRangeException( Strings.ExceptionPlaylistIndexOutOfRange );
						}
					}

					m_selectedIndex = newIndex;
					fireEvent = true;
				}

				UpdateSelectedItem( listChanged );
			}

			if ( fireEvent )
			{
				OnPropertyChanged( new PropertyChangedEventArgs( "SelectedIndex" ) );
			}
		}

		/// <summary>
		/// Updates the SelectedItem property based on the SelectedIndex property
		/// </summary>
		/// <param name="listChanged">
		///		If true, the track won't restart if it didn't change
		///	</param>
		private void UpdateSelectedItem( bool listChanged )
		{
			bool fireEvent = false;

			lock ( ((ICollection) this).SyncRoot )
			{
				PlaylistItemBase newItem = null;

				if ( Count > 0 )
				{
					newItem = this[SelectedIndex];
				}

				PlaylistItemBase.PlayState? oldPlaystate = null;

				if ( m_selectedItem != null )
				{
					oldPlaystate = m_selectedItem.CurrentPlayState;
				}

				if ( m_selectedItem != newItem )
				{
					if ( m_selectedItem != null )
					{
						m_selectedItem.Stop();
						m_selectedItem.Selected = false;
					}

					m_selectedItem = newItem;

					if ( m_selectedItem != null )
					{
						m_selectedItem.Selected = true;
					}

					fireEvent = true;
				}
				else if ( !listChanged )
				{
					m_selectedItem.Reset();
				}

				AdjustPlayState( oldPlaystate ?? PlaylistItemBase.PlayState.Stopped );
			}

			if ( fireEvent && TrackChanged != null )
			{
				TrackChanged( this, new PropertyChangedEventArgs( "SelectedItem" ) );
			}
		}

		/// <summary>
		/// Updates the SelectedIndex property based on the SelectedItem property
		/// </summary>
		/// <param name="listChanged">
		///		If true, the track won't restart if it didn't change
		///	</param>
		private void UpdateSelectedIndex( bool listChanged )
		{
			int newIndex = 0;

			lock ( ((ICollection) this).SyncRoot )
			{
				if ( SelectedItem != null && Contains( SelectedItem ) )
				{
					newIndex = IndexOf( SelectedItem );
				}
			}

			SetSelectedIndex( newIndex, listChanged );
		}

		private PlaylistItemBase m_selectedItem;

		/// <summary>
		/// Currently selected track (item) in the playlist
		/// </summary>
		public PlaylistItemBase SelectedItem
		{
			get
			{
				lock ( ((ICollection) this).SyncRoot )
				{
					return m_selectedItem;
				}
			}
			set
			{
				try
				{
					SelectedIndex = IndexOf( value );
				}
				catch ( Exception )
				{
					throw new ArgumentException( Strings.ExceptionPlaylistItemNotFound );
				}
			}
		}

		private bool m_isPlaying;

		/// <summary>
		/// Is this playlist currently playing?
		/// </summary>
		public bool IsPlaying
		{
			get
			{
				return m_isPlaying;
			}

			set
			{
				bool fireEvent = false;
				lock ( ((ICollection) this).SyncRoot )
				{
					if ( m_isPlaying != value )
					{
						m_isPlaying = value;
						fireEvent = true;
					}
				}

				if ( fireEvent )
				{
					OnPropertyChanged( new PropertyChangedEventArgs( "IsPlaying" ) );
				}
			}
		}

		/// <summary>
		/// The number of atomic transactions currently going on in this playlist
		/// </summary>
		private HashSet<int> m_transactions = new HashSet<int>();

		/// <summary>
		/// The ID of the latest started transaction
		/// </summary>
		private int m_maxTransactionId = 0;

		/// <summary>
		/// Signals that an atomic transaction is going on in this playlist
		/// </summary>
		public bool TransactionGoing
		{
			get
			{
				lock ( ((ICollection) this).SyncRoot )
				{
					return m_transactions.Count != 0;
				}
			}
		}


		#endregion Properties

		#region Events

		/// <summary>
		/// Event for changes to the SelectedItem / SelectedIndex
		/// </summary>
		public event PropertyChangedEventHandler TrackChanged;

		/// <summary>
		/// Signals the start of an atomic transaction in this playlist
		/// </summary>
		public event EventHandler TransactionStarted;

		/// <summary>
		/// Signals the end of an atomic transaction in this playlist.
		/// The data in the event args tell you whether all transactions have ended.
		/// </summary>
		public event EventHandler<CustomDataEventArgs<bool>> TransactionEnded;

		#endregion Events

		#region Constructors

		/// <summary>
		/// Creates an empty playlist
		/// </summary>
		/// <param name="name">the name of this playlist</param>
		public Playlist( string name )
		{
			Name = name;

			ItemAdded += Playlist_ItemAdded;
			ItemMoved += Playlist_ItemMoved;
			ItemRemoved += Playlist_ItemRemoved;
			ItemsCleared += Playlist_ItemsCleared;
			ItemReplaced += Playlist_ItemReplaced;
		}

		#endregion Constructors

		#region Playback methods

		/// <summary>
		/// Steps to the next item in the playlist
		/// </summary>
		public void Next()
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				if ( Count == 0 )
				{
					return;
				}

				if ( RepeatMode == RepeatModeType.RepeatOne )
				{
					UpdateSelectedItem( false ); // this will restart it
				}
				else if ( Shuffle ) // TODO: csak akkor, ha még van olyan szám, ami nincs a stackben, és csak azokat adja be
				{
					int index = 0;

					do
					{
						index = m_shuffleRandom.Next( Count );
					}
					while ( !(index != SelectedIndex || Count <= 1) );

					SelectedIndex = index;
				}
				else if ( RepeatMode == RepeatModeType.RepeatAll || SelectedIndex < Count - 1 )
				{
					SelectedIndex = ((SelectedIndex + 1) % Count);
				}
				else
				{
					Stop();
				}
			}
		}

		/// <summary>
		/// Steps to the previous item in the playlist
		/// </summary>
		public void Previous()
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				if ( Count == 0 )
				{
					return;
				}

				if ( RepeatMode == RepeatModeType.RepeatOne )
				{
					UpdateSelectedItem( false ); // this will restart it
				}
				else if ( Shuffle ) // TODO: legyen egy stack, amibe bekerülnek a lejátszott számok és azt popolja
				{
					int index = 0;

					do
					{
						index = m_shuffleRandom.Next( Count );
					}
					while ( (index != SelectedIndex || Count <= 1) && (Math.Abs( index - SelectedIndex ) > 1 || Count <= 2) );

					SelectedIndex = index;
				}
				else if ( RepeatMode == RepeatModeType.RepeatAll || SelectedIndex > 0 )
				{
					SelectedIndex = ((SelectedIndex - 1 + Count) % Count);
				}
				else
				{
					Stop();
				}
			}
		}

		/// <summary>
		/// Adjusts the playstate of the selected item based on the passed old playstate.
		/// Needs to be called after advancing the track in next/previous.
		/// </summary>
		/// <param name="oldPlaystate"></param>
		private void AdjustPlayState( PlaylistItemBase.PlayState oldPlaystate )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				if ( SelectedItem == null )
				{
					return;
				}

				switch ( oldPlaystate )
				{
					case PlaylistItemBase.PlayState.Stopped:
						SelectedItem.Stop();
						IsPlaying = false;
						break;
					case PlaylistItemBase.PlayState.Paused:
						SelectedItem.Pause();
						IsPlaying = false;
						break;
					case PlaylistItemBase.PlayState.Playing: /* fallthrough */
					case PlaylistItemBase.PlayState.Seeking: /* fallthrough */
					case PlaylistItemBase.PlayState.Finished: /* fallthrough */
					case PlaylistItemBase.PlayState.Error:
						{
							bool foundNonError = false; // there is an item not in error state

							foreach ( var item in this )
							{
								if ( item.CurrentPlayState != PlaylistItemBase.PlayState.Error
									&& item.CurrentPlayState != PlaylistItemBase.PlayState.Initializing )
								{
									foundNonError = true;
									break;
								}
							}

							if ( foundNonError && IsPlaying )
							{
								SelectedItem.Play();
							}
							else
							{
								SelectedItem.Stop();
								IsPlaying = false;
							}
							break;
						}
				}
			}
		}

		/// <summary>
		/// Immediately starts playback of the selected track from the selected position
		/// </summary>
		public void Play()
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				PlaylistItemBase currentItem = SelectedItem;

				if ( currentItem != null )
				{
					IsPlaying = true;
					currentItem.Play();
				}
			}
		}

		/// <summary>
		/// Sets the selected track as the currently played on the attached player and pauses it
		/// </summary>
		public void Pause()
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				PlaylistItemBase currentItem = SelectedItem;

				if ( currentItem != null )
				{
					currentItem.Pause();
					IsPlaying = false;
				}
			}
		}

		/// <summary>
		/// Stops the playback (or does nothing if it is stopped)
		/// </summary>
		public void Stop()
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				PlaylistItemBase currentItem = SelectedItem;

				if ( currentItem != null )
				{
					currentItem.Stop();
					IsPlaying = false;
				}
			}
		}

		#endregion Playback methods

		#region Other methods

		/// <summary>
		/// Copies the content of the passed playlist into this one.
		/// The items will be clones of the original objects (they can be shallow, mixed or deep copies, depending on their implementation).
		/// </summary>
		/// <param name="other"></param>
		public void SyncFrom( Playlist other )
		{
			bool fireEvent = false;

			lock ( ((ICollection) other).SyncRoot )
				lock ( ((ICollection) this).SyncRoot )
				{
					int transactionId = StartTransaction();
					try
					{
						bool suppressed = NotificationsSuppressed;

						try
						{
							NotificationsSuppressed = true;
							Clear();

							foreach ( var item in other )
							{
								Add( item.Clone() );
							}
						}
						finally
						{
							NotificationsSuppressed = suppressed;
							fireEvent = true;
						}

						SelectedIndex = other.SelectedIndex;
					}
					finally
					{
						EndTransaction( transactionId );
					}
				}

			if ( fireEvent )
			{
				OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
			}
		}

		/// <summary>
		/// Starts a new atomic transaction on this playlist. Watchers of this collection
		/// may consider waiting for the end of the transaction to process its results.
		/// </summary>
		/// <returns>the ID of the newly started transaction</returns>
		public int StartTransaction()
		{
			int id = 0;

			lock ( ((ICollection) this).SyncRoot )
			{
				id = ++m_maxTransactionId;

				m_transactions.Add( id );
			}

			if ( TransactionStarted != null )
			{
				TransactionStarted( this, EventArgs.Empty );
			}

			return id;
		}

		/// <summary>
		/// Ends an atomic transaction on this playlist. Watchers of this collection
		/// may consider waiting for the end of the transaction to process its results.
		/// </summary>
		/// <param name="transactionId">the ID of the transaction to end</param>
		public void EndTransaction( int transactionId )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				m_transactions.Remove( transactionId );
			}

			if ( TransactionEnded != null )
			{
				TransactionEnded( this, new CustomDataEventArgs<bool>( !TransactionGoing ) );
			}
		}

		#endregion Other methods

		#region Playlist change observation and updates

		private void Playlist_ItemAdded( object sender, CollectionItemAddedEventArgs e )
		{
			((PlaylistItemBase) e.Item).Parent = this;

			UpdateSelectedIndex( true );

			if ( SelectedItem != null && IsPlaying )
			{
				SelectedItem.Play();
			}
			else
			{
				IsPlaying = false;
			}
		}

		private void Playlist_ItemMoved( object sender, CollectionItemMovedEventArgs e )
		{
			if ( m_selectedIndex == e.OldIndex )
			{
				m_selectedIndex = e.NewIndex;
			}
			else if ( m_selectedIndex < e.OldIndex && m_selectedIndex >= e.NewIndex )
			{
				m_selectedIndex++;
			}
			else if ( m_selectedIndex > e.OldIndex && m_selectedIndex <= e.NewIndex )
			{
				m_selectedIndex--;
			}
		}

		private void Playlist_ItemRemoved( object sender, CollectionItemRemovedEventArgs e )
		{
			((PlaylistItemBase) e.Item).Parent = null;
			((PlaylistItemBase) e.Item).Selected = false;

			if ( Count == 0 )
			{
				SetSelectedIndex( 0, listChanged: true );
			}
			else if ( e.Index == SelectedIndex )
			{
				SetSelectedIndex( SelectedIndex % Count, listChanged: true ); // jump to next
			}
			else
			{
				UpdateSelectedIndex( true );
			}

			if ( SelectedItem != null && IsPlaying )
			{
				SelectedItem.Play();
			}
			else
			{
				IsPlaying = false;
			}
		}

		private void Playlist_ItemsCleared( object sender, CollectionItemsClearedEventArgs e )
		{
			foreach ( var item in e.Items )
			{
				((PlaylistItemBase) item).Parent = null;
				((PlaylistItemBase) item).Selected = false;

				if ( ((PlaylistItemBase) item).CurrentPlayState == PlaylistItemBase.PlayState.Playing || ((PlaylistItemBase) item).CurrentPlayState == PlaylistItemBase.PlayState.Seeking )
				{
					((PlaylistItemBase) item).Pause();
				}
			}

			SetSelectedIndex( 0, listChanged: true );

			IsPlaying = false;
		}

		private void Playlist_ItemReplaced( object sender, CollectionItemReplacedEventArgs e )
		{
			((PlaylistItemBase) e.OldItem).Parent = null;
			((PlaylistItemBase) e.OldItem).Selected = false;

			if ( ((PlaylistItemBase) e.OldItem).CurrentPlayState == PlaylistItemBase.PlayState.Playing || ((PlaylistItemBase) e.OldItem).CurrentPlayState == PlaylistItemBase.PlayState.Seeking )
			{
				((PlaylistItemBase) e.OldItem).Pause();
			}

			((PlaylistItemBase) e.NewItem).Parent = this;

			if ( e.NewItem != e.OldItem )
			{
				SetSelectedIndex( SelectedIndex % Count, listChanged: true ); // Fire the events
			}

			if ( SelectedItem != null && IsPlaying )
			{
				SelectedItem.Play();
			}
			else
			{
				IsPlaying = false;
			}
		}

		#endregion Playlist change observation and updates
	}
}
